package com.tianji.learning.service.impl;

import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.api.client.course.CourseClient;
import com.tianji.api.dto.IdAndNumDTO;
import com.tianji.api.dto.course.CourseSimpleInfoDTO;
import com.tianji.api.dto.leanring.LearningLessonDTO;
import com.tianji.api.dto.leanring.LearningRecordDTO;
import com.tianji.api.dto.leanring.LearningRecordFormDTO;
import com.tianji.common.constants.RedisConstant;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.JsonUtils;
import com.tianji.common.utils.StringUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.learning.domain.po.LearningLesson;
import com.tianji.learning.domain.po.LearningRecord;
import com.tianji.learning.enums.LessonStatus;
import com.tianji.learning.enums.SectionType;
import com.tianji.learning.mapper.LearningRecordMapper;
import com.tianji.learning.service.ILearningLessonService;
import com.tianji.learning.service.ILearningRecordService;
import com.tianji.learning.util.DelayedTask;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 学习记录表 服务实现类
 * </p>
 *
 * @author xjs
 */
@Slf4j
@Service
public class LearningRecordServiceImpl extends ServiceImpl<LearningRecordMapper, LearningRecord> implements ILearningRecordService {

    @Autowired
    @Lazy
    private ILearningLessonService lessonService;

    @Autowired
    private CourseClient courseClient;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private PlatformTransactionManager txManager;

    /**
     * 用来存放延迟消息的队列
     * */
    private DelayQueue<DelayedTask<LearningRecord>> learningRecordDelayQueue = new DelayQueue<>();

    /**
     * 用来从delayqueue获取任务
     * */
    private ThreadPoolExecutor takeTaskPool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));

    /**
     * 用来执行任务
     * */
    private ThreadPoolExecutor executeTaskPool = new ThreadPoolExecutor(
            Runtime.getRuntime().availableProcessors(),
            Runtime.getRuntime().availableProcessors()*2,
            120, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(Runtime.getRuntime().availableProcessors()),
            new ThreadPoolExecutor.DiscardOldestPolicy());

    /**
     * spring生命周期回调，在这个回调中从delay queue取任务
     * */
    @PostConstruct
    public void postConstruct(){
        takeTaskPool.submit(new Runnable() {
            @Override
            public void run() {
                // 为了防止阻塞主线程，所以放到线程池异步执行
                while(true){
                    try{
                        // 从延迟队列取任务
                        DelayedTask<LearningRecord> task = learningRecordDelayQueue.take();
                        // 为了防止任务的阻塞，把任务的执行放到单独的线程池
                        executeTaskPool.submit(new Runnable() {
                            @Override
                            public void run() {
                                // 从task中取出旧的播放记录
                                LearningRecord record = task.getData();
                                log.info("延迟消息出队：{}", JsonUtils.toJsonStr(record));
                                // 同步到DB
                                syncLearningRecordCacheDataToDB(record);
                            }
                        });
                    }catch(Exception e){
                        log.error("取任务异常", e);
                    }
                }
            }
        });
    }

    private void syncLearningRecordCacheDataToDB(LearningRecord taskRecord){
        if(taskRecord == null){
            log.error("获取任务为空");
            return;
        }
        // 比较cache中的进度和task中的进度
        String key = getLearningRecordKey(taskRecord.getUserId(), taskRecord.getLessonId());
        String json = (String)redisTemplate.opsForHash().get(key, "" + taskRecord.getSectionId());
        if(StringUtils.isEmpty(json)){
            log.error("redis中的学习记录为空");
            return;
        }
        LearningRecord cacheRecord = JsonUtils.toBean(json, LearningRecord.class);
        // 如果不一样，说明还在播放，不用管
        if(cacheRecord.getMoment() - taskRecord.getMoment() != 0){
            log.info("视频持续播放中，无需更新，record：{}", JsonUtils.toJsonStr(taskRecord));
            return;
        }
        log.info("用户已经退出视频，需更新，record：{}", JsonUtils.toJsonStr(taskRecord));
        // 否则
        // 编程式事务处理
        TransactionDefinition transactionDefinition = TransactionDefinition.withDefaults();
        TransactionStatus transaction = txManager.getTransaction(transactionDefinition);
        try{
            // 更新learning_record
            this.updateById(taskRecord);
            // 更新learning_lesson
            LearningRecordFormDTO dto = new LearningRecordFormDTO();
            dto.setLessonId(taskRecord.getLessonId());
            dto.setSectionId(taskRecord.getSectionId());
            dto.setCommitTime(taskRecord.getUpdateTime());
            updateLearnLesson(dto, false);
            // 提交事务
            txManager.commit(transaction);
        }catch(Exception e){
            log.error("更新learning_record和learning_lesson异常", e);
            // 回滚事务
            txManager.rollback(transaction);
        }
    }

    @Override
    public List<IdAndNumDTO> queryFinishedSectionNum(Long userId, LocalDateTime begin, LocalDateTime end) {
        return this.getBaseMapper().queryFinishedSectionNum(userId, begin, end);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addLearningRecord(LearningRecordFormDTO dto) {
        Long userId = UserContext.getUser();
        // 判断视频还是考试
        Integer sectionType = dto.getSectionType();
        if(SectionType.EXAM.getValue() == sectionType){
            // 考试
            // 新增学习记录
            addLearnRecord(dto, true);
            // 更新课表
            updateLearnLesson(dto, true);
        }else{
            // 视频， 先查缓存，再查库
            LearningRecord recordDB = getByLessonIdSectionId(userId, dto.getLessonId(), dto.getSectionId());
            if(recordDB == null){
                // 第一次提交
                // 新增学习记录
                addLearnRecord(dto, false);
                // 更新课表
                updateLearnLesson(dto, false);
            }else{
                // 不是第一次提交
                Integer total = dto.getDuration();
                Integer moment = dto.getMoment();
                // 是否是第一次超过50%： 1)还没到50% 2)早就超了50%,现在可能是90%
                boolean firstFinish = (moment * 2 >= total) && (!recordDB.getFinished());
                if(!firstFinish){
                    log.info("firstFinish false, 提交学习记录：{}", JsonUtils.toJsonStr(dto));
                    // 如果不是，则只需要更新播放进度，更新课表的最近的小节和最近的时间
                    // 用户在观看视频的过成功当中，为了减少数据库写入的次数，不需要实时的把进度信息写入到数据库
                    // 可以先暂存到redis，同时提交延迟任务
                    // 任务到期以后，检查任务中的进度和redis中的进度是否一致，如果一致则说明已经退出不再观看视频，则需要把redis中的数据同步到数据库
                    // 更新redis
                    updateLearningRecordCache(recordDB, dto, false);
                    // 提交延迟任务,20秒  //TODO 把这个时间放到nacos配置中心
                    log.info("发送延迟消息：{}", JsonUtils.toJsonStr(recordDB));
                    DelayedTask<LearningRecord> task = new DelayedTask<>(recordDB, 20, TimeUnit.SECONDS);
                    learningRecordDelayQueue.put(task);
                }else{
                    // 更新学习记录
                    updateLearningRecord(recordDB.getId(), dto, true);
                    // 更新课表
                    updateLearnLesson(dto, true);
                    // 更新redis
                    updateLearningRecordCache(recordDB, dto, true);
                }
            }
        }
    }

    /**
     * 更新redis中缓存的学习记录
     * */
    private void updateLearningRecordCache(LearningRecord recordDB, LearningRecordFormDTO dto, boolean finish){
        recordDB.setMoment(dto.getMoment());
        recordDB.setUpdateTime(dto.getCommitTime());
        if(finish){
            recordDB.setFinished(true);
            recordDB.setFinishTime(dto.getCommitTime());
        }
        // 获取redis中的key
        String key = getLearningRecordKey(recordDB.getUserId(), recordDB.getLessonId());
        redisTemplate.opsForHash().put(key, ""+recordDB.getSectionId(), JsonUtils.toJsonStr(recordDB));
        log.info("updateLearningRecordCache, 更新前：{}，更新后：{}", JsonUtils.toJsonStr(recordDB), JsonUtils.toJsonStr(dto));
    }

    private void addLearnRecord(LearningRecordFormDTO dto, boolean finish){
        LearningRecord record = BeanUtils.copyBean(dto, LearningRecord.class);
        record.setUserId(UserContext.getUser());
        if(finish){
            record.setFinishTime(dto.getCommitTime());
            record.setFinished(true);
        }
        this.save(record);
    }

    private void updateLearnLesson(LearningRecordFormDTO dto, boolean addSectionNum){
        LearningLesson lesson = lessonService.getById(dto.getLessonId());
        if (lesson == null){
            throw new BizIllegalException("lesson empty");
        }
        //如果是未开始 则改为学习中
        if(lesson.getStatus() == LessonStatus.NOT_BEGIN.getValue()){
            lesson.setStatus(LessonStatus.LEARNING.getValue());
        }
        // 假如说需要增加小节数量
        if(addSectionNum){
            Integer oldLearnedSections = lesson.getLearnedSections();
            lesson.setLearnedSections(oldLearnedSections + 1);
            // 查课程 拿到章节的总数
            // 查询课程，去获取课程的章节总数
            List<CourseSimpleInfoDTO> courseList = courseClient.getSimpleInfoList(Arrays.asList(lesson.getCourseId()));
            if(CollUtil.isEmpty(courseList)){
                throw new BizIllegalException("course empty");
            }
            if(oldLearnedSections+1 >= courseList.get(0).getSectionNum()){
                // 已经学完
                lesson.setStatus(LessonStatus.FINISHED.getValue());
            }
        }
        lesson.setLatestLearnTime(dto.getCommitTime());
        lesson.setLatestSectionId(dto.getSectionId());
        lessonService.updateById(lesson);
    }

    private void updateLearningRecord(Long recordId, LearningRecordFormDTO dto, boolean firstFinish) {
        LearningRecord update = BeanUtils.copyBean(dto, LearningRecord.class);
        update.setId(recordId);
        if(firstFinish){
            update.setFinished(true);
            update.setFinishTime(dto.getCommitTime());
        }
        this.updateById(update);
    }

    //@Override
    public void addLearningRecord2(LearningRecordFormDTO dto) {
        Long userId = UserContext.getUser();
        // 判断视频还是考试
        Integer sectionType = dto.getSectionType();
        if(SectionType.EXAM.getValue() == sectionType){
            // 考试
            // 新增学习记录  finished=true
            LearningRecord record = BeanUtils.copyBean(dto, LearningRecord.class);
            record.setUserId(userId);
            record.setFinished(true);
            record.setFinishTime(dto.getCommitTime());
            this.save(record);
            //   更新课表中相关的数据
            updateLearingLesson(dto, true);
        }else{
            // 视频
            //  是否是第一次提交，先查缓存再查数据库
            LearningRecord recordDB = getByLessonIdSectionId(userId, dto.getLessonId(), dto.getSectionId());
            if(recordDB == null){
                //  是第一次提交
                //  新增学习记录  finished=false
                LearningRecord record = BeanUtils.copyBean(dto, LearningRecord.class);
                record.setUserId(userId);
                this.save(record);
                // 更新课表中 最近学习的小节  和 最近学习的时间
                updateLearingLesson(dto, false);
            }else{
                // 不是第一次提交
                // 第一次超过50%
                Integer total = dto.getDuration();
                Integer moment = dto.getMoment();
                boolean firstFinish = (moment * 2 >= total) && (!recordDB.getFinished());
                // 更新学习记录
                LearningRecord recordUpdate = BeanUtils.copyBean(dto, LearningRecord.class);
                recordUpdate.setId(recordDB.getId());
                if(firstFinish){
                    recordUpdate.setFinished(true);
                    recordUpdate.setFinishTime(dto.getCommitTime());
                }
                this.updateById(recordUpdate);
                // 更新课表
                updateLearingLesson(dto, firstFinish);
            }
        }
    }

    /**
     * 获取redis中学习记录的key
     * */
    private String getLearningRecordKey(Long userId, Long lessonId){
        String keyTemplate = RedisConstant.LEARNING_RECORD_PREFIX + "{}:{}";
        String bigKey = StringUtils.format(keyTemplate, userId, lessonId);
        return bigKey;
    }

    @Override
    public LearningRecord getByLessonIdSectionId(Long userId, Long lessonId, Long sectionId) {
        // 查缓存，hash结构，大key：userId:lessonId, 小key：sectionId， value：json格式数据
        String bigKey = getLearningRecordKey(userId, lessonId);
        String json = (String)redisTemplate.opsForHash().get(bigKey, "" + sectionId);
        if(!StringUtils.isEmpty(json)){
            return JsonUtils.toBean(json, LearningRecord.class);
        }
        //如果缓存中没有，查数据库
        LambdaQueryWrapper<LearningRecord> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(LearningRecord::getLessonId, lessonId)
                .eq(LearningRecord::getSectionId, sectionId);
        LearningRecord record = this.getOne(queryWrapper);
        if(record != null){
            // 查到了，存入redis
            redisTemplate.opsForHash().put(bigKey,""+sectionId, JsonUtils.toJsonStr(record));
            // 设置有效期
            redisTemplate.expire(bigKey, Duration.ofDays(30));
        }
        return record;
    }

    @Override
    public LearningLessonDTO queryLearningRecordByCourse(Long courseId) {
        Long userId = UserContext.getUser();
        // 跟据courseId 查询learn_lesson拿到latest_section_id， lesson_id
        LearningLesson lesson = lessonService.queryByUserIdCourseId(userId, courseId);
        // 查不到或者已经过期
        if(lesson == null || Objects.equals(lesson.getStatus(), LessonStatus.EXPIRED.getValue())){
            throw  new BizIllegalException("查询不到课表信息");
        }
        // 根据lesson_id 查询学习记录列表
        Long lessonId = lesson.getId();
        LambdaQueryWrapper<LearningRecord> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(LearningRecord::getUserId,userId ).eq(LearningRecord::getLessonId, lessonId);
        List<LearningRecord> records = this.list(queryWrapper);
        // 组装返回
        LearningLessonDTO dto = new LearningLessonDTO();
        dto.setId(lessonId);
        dto.setLatestSectionId(lesson.getLatestSectionId());
        dto.setRecords(CollUtil.isEmpty(records)?null:BeanUtils.copyList(records, LearningRecordDTO.class));
        return dto;
    }

    private void updateLearingLesson(LearningRecordFormDTO dto, boolean firstFinish){
        LearningLesson lessonDB = lessonService.getById(dto.getLessonId());
        if(lessonDB == null){
            throw new BizIllegalException("lesson empty");
        }
        if(firstFinish){
            //   - 课表中以学习的小节数量+1
            lessonDB.setLearnedSections(lessonDB.getLearnedSections()+1);
            // 查询课程，去获取课程的章节总数
            List<CourseSimpleInfoDTO> courseList = courseClient.getSimpleInfoList(Arrays.asList(lessonDB.getCourseId()));
            if(CollUtil.isEmpty(courseList)){
                throw new BizIllegalException("course empty");
            }
            //  - 判断是否学完, 已学习的数量>=课程的总数量
            CourseSimpleInfoDTO course = courseList.get(0);
            if(lessonDB.getLearnedSections() >= course.getSectionNum()){
                lessonDB.setStatus(LessonStatus.FINISHED.getValue());
            }
        }
        //  - 更新课表中 最近学习的小节  和 最近学习的时间
        lessonDB.setLatestLearnTime(dto.getCommitTime());
        lessonDB.setLatestSectionId(dto.getSectionId());
        lessonService.updateById(lessonDB);
    }
}
